/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.generator;

import freemarker.template.TemplateException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Element;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import redora.generator.Template.Input;
import redora.generator.exceptions.ModelGenerationException;
import redora.util.BusinessRuleMessageUtils;
import redora.util.DefaultErrorHandler;
import redora.util.DocPrint;
import redora.util.SchemaValidator;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * Retrieves all available models from the file system in this project and
 * generated all the sources and resources with the available templates. The
 * maven plugin will use the ModelProcessor directly, so just add the maven
 * plugin to your project's plugins.
 *
 * @author jaap
 */
public class ModelProcessor {

    public static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
    public static final String XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
    public static final String SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
    /**
     * Schema file for checking if the model document is correct.
     */
    public static final String MODEL_SCHEMA = "/model.xsd";
    /**
     * Schema file for checking if the include document is correct.
     */
    public static final String INCLUDE_SCHEMA = "/include.xsd";
    /**
     * Location of the upgrade scripts in resources.
     */
    public static final String UPGRADE = "upgrade";

    /**
     * Generate sources for project
     */
    public static void generate(String basePackage, String defaultLanguage, String sourceDir,
                                String buildDir, String resourceDir, String artifactId)
            throws ParserConfigurationException, TemplateException, XPathExpressionException,
            TransformerException, ModelGenerationException, SAXException, IOException {
        TemplateHandler t = new TemplateHandler();
        ModelFileHandler p = new ModelFileHandler(sourceDir, buildDir, resourceDir);
        Document allModels = newDocument(basePackage, defaultLanguage, "objects");
        upgradeFiles(resourceDir, allModels);

        // Check the model files if all is ready to go
        for (String modelFile : p.findModelFiles()) {
            try {
                if (ModelProcessor.invalidToSchema(MODEL_SCHEMA, p.modelDir() + modelFile)) {
                    throw new ModelGenerationException("Model file " + modelFile
                            + " failed the schema validation, i will stop now."
                            + ModelGenerationException.printModel(p, modelFile));
                }
            } catch (SAXException e) {
                throw new ModelGenerationException("Model file " + modelFile + " failed. "
                        + ModelGenerationException.printModel(p, modelFile), e);
            } catch (IOException e) {
                throw new ModelGenerationException("Model file " + modelFile + " failed. "
                        + ModelGenerationException.printModel(p, modelFile), e);
            }
        }
        System.out.println("All model files were checked as valid against model.xsd");

        for (String includeFile : p.findIncludeFiles()) {
            try {
                if (ModelProcessor.invalidToSchema(INCLUDE_SCHEMA, p.includeDir() + includeFile)) {
                    throw new ModelGenerationException("Include file " + includeFile
                            + " failed the schema validation, i will stop now."
                            + ModelGenerationException.printInclude(p, includeFile));
                }
            } catch (SAXException e) {
                throw new ModelGenerationException("Include file " + includeFile + " failed. "
                        + ModelGenerationException.printInclude(p, includeFile), e);
            } catch (IOException e) {
                throw new ModelGenerationException("Could not open this include: " + includeFile, e);
            }
        }
        if (!p.findIncludeFiles().isEmpty()) {
            System.out.println("All include files were checked as valid against include.xsd");
        }

        // Run the per model generation stuff
        int sequence = 0;
        for (String modelFile : p.findModelFiles()) {
            String modelName = modelFile.replace(".xml", "");
            Document model = model(p.modelDir(), modelName, basePackage, sequence);
            allModels.getFirstChild()
                    .appendChild(allModels.importNode(model.getFirstChild(), true));

            for (Template tpl : t.byInput(Input.Model)) {
                if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
                    p.process(tpl, model, basePackage + "." + tpl.packageSuffix, tpl
                            .getDestinationFileName(modelName, null, null), null);
                } else {
                    System.out.println("Ignoring " + tpl.name + " for " + artifactId);
                }
            }
        }

        for (Template tpl : t.byInput(Input.AllModels)) {
            if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
                p.process(tpl, allModels, basePackage + "." + tpl.packageSuffix, tpl
                        .getDestinationFileName(null, null, null), null);
            } else {
                System.out.println("Ignoring " + tpl.name + " for " + artifactId);
            }
        }

        Map<String, Document> enumerations = XMLUtil.enumerations(allModels, basePackage);
        for (String key : enumerations.keySet()) {
            System.out.println("Processing enum " + key);
            for (Template tpl : t.byInput(Input.Enum)) {
                if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
                    p.process(tpl, enumerations.get(key), basePackage + "." + tpl.packageSuffix,
                            tpl.getDestinationFileName(null, null, key), null);
                } else {
                    System.out.println("Ignoring " + tpl.name + " for " + artifactId);
                }
            }
        }

        for (String language : XMLUtil.definedLanguages(allModels)) {
            HashMap<String, String> langParam = new HashMap<String, String>();
            langParam.put("language", language);
            System.out.println("Processing language " + language);
            for (Template tpl : t.byInput(Input.Language)) {
                if (tpl.ignoreProjects == null || !tpl.ignoreProjects.contains(artifactId)) {
                    p.process(tpl, allModels, basePackage + "." + tpl.packageSuffix, tpl
                            .getDestinationFileName(null, language, null), langParam);
                } else {
                    System.out.println("Ignoring " + tpl.name + " for " + artifactId);
                }
            }
        }
    }

    /**
     * Create the model XML document and add the <name/> tag so the name can
     * also can be parsed by FreeMarker.
     *
     * @param modelName   The file name is used as model name and put to the 'name'
     *                    element in the document
     * @param modelDir    The model directory
     * @param packageName (Optional) Adds a 'package' element, it will the the root's
     *                    first child
     * @return Document The Model document
     */
    public static Document model(String modelDir, String modelName, String packageName, int sequence)
            throws ModelGenerationException {
        Document doc;
        String modelFileName = modelDir + modelName + ".xml";
        try {
            doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
                    new File(modelFileName));
            // The file name is also the object name!
            addChildElement(doc, "name", modelName);
            // The package is for all objects the same and it is set in the
            // maven pom.
            addChildElement(doc, "package", packageName);
            addChildElement(doc, "sequence", String.valueOf(sequence++));
            loadIncludes(doc, modelDir);
            normalize(doc);
            addScope(doc);
        } catch (SAXException e) {
            throw new ModelGenerationException("Strange, i failed to create a DOM of this model "
                    + modelName + " (" + modelFileName + ")", e);
        } catch (IOException e) {
            throw new ModelGenerationException("I failed to find this file: " + modelFileName, e);
        } catch (ParserConfigurationException e) {
            throw new ModelGenerationException(
                    "Strange, the DOM is not configured right, it worked before! Maybe you have some system properties set that interfere with me"
                            + " (" + modelName + ")", e);
        }
        return doc;
    }

    /**
     * Create the model XML document and adds the root element and optional a
     * package element. This method is simple and created for your convenience.
     *
     * @param basePackage     (Optional) Adds a 'package' element, it will the the root's
     *                        first child
     * @param defaultLanguage (Optional) Adds a 'defaultLanguage' element
     * @param root            (Mandatory) root element name
     * @return Document The Model document
     */
    public static Document newDocument(String basePackage, String defaultLanguage, String root)
            throws ModelGenerationException {
        Document doc;

        try {
            doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        } catch (ParserConfigurationException e) {
            throw new ModelGenerationException(
                    "Strange, the DOM is not configured right, it worked before! Maybe you have some system properties set that interfere with me",
                    e);
        }

        Node insertNode = doc.createElement(root);
        doc.appendChild(doc.importNode(insertNode, true));
        if (basePackage != null) {
            addChildElement(doc, "package", basePackage);
        }
        if (defaultLanguage != null) {
            addChildElement(doc, "defaultLanguage", defaultLanguage);
        }
        return doc;
    }

    /**
     * Adds an child element to the document's root element.
     *
     * @param doc   (Mandatory) The XML document
     * @param tag   (Mandatory) The element's name
     * @param value (Optional) It's value
     */
    public static void addChildElement(Document doc, String tag, String value) {
        Node tagNode = doc.createElement(tag);
        if (value != null) {
            tagNode.setTextContent(value);
        }
        doc.getFirstChild().appendChild(tagNode);
    }

    /**
     * Validate the xml file according to schema file MODEL_SCHEMA
     *
     * @param xmlFileName xmlFileName
     * @return True if the XML file does not match the XSD.
     * @throws ModelGenerationException
     */
    @SuppressWarnings("unchecked")
    public static boolean invalidToSchema(String schemaFile, String xmlFileName)
            throws ModelGenerationException, SAXException, IOException {
        File xmlFile = new File(xmlFileName);
        DefaultErrorHandler errorHandler = new DefaultErrorHandler();

        if (!xmlFile.exists()) {
            throw new ModelGenerationException("xml file:" + xmlFileName + " does not exist");
        }
        SchemaValidator validator;
        if (schemaFile.equalsIgnoreCase(INCLUDE_SCHEMA)) {
            String include = IOUtils.toString(ModelProcessor.class
                    .getResourceAsStream(INCLUDE_SCHEMA));
            String model = IOUtils.toString(ModelProcessor.class.getResourceAsStream(MODEL_SCHEMA));
            InputStream includeStream = IOUtils.toInputStream(include.replace(
                    "<xs:include schemaLocation=\"model.xsd\" />", model.substring(model
                            .indexOf("<xs:complexType name=\"attributesType\">"), model
                            .lastIndexOf("</xs:schema>"))));
            validator = new SchemaValidator(includeStream);
        } else {
            validator = new SchemaValidator(ModelProcessor.class.getResourceAsStream(MODEL_SCHEMA));
        }

        validator.validate(xmlFile, errorHandler);

        if (errorHandler.getErrors().hasContent()) {
            System.out.println("XML file:" + xmlFileName
                    + " failed to do checking accroding to XSD file:");
            Iterator<Element> ei = errorHandler.getErrors().elementIterator();
            while (ei.hasNext()) {
                Element el = ei.next();
                System.out.println(el.getStringValue());
            }

            return true;
        }

        return false;
    }

    /**
     * Search the model document for include tags. Then replace these tags with
     * their corresponding include files.
     *
     * @param doc      The XML document
     * @param modelDir
     */
    public static void loadIncludes(Document doc, String modelDir) throws ModelGenerationException {
        NodeList includes;
        try {
            includes = (NodeList) XPathFactory.newInstance().newXPath().evaluate("//include", doc,
                    XPathConstants.NODESET);
        } catch (XPathExpressionException e) {
            throw new ModelGenerationException("Failed in searching include", e);
        }
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        // All includes are different because attributes can't be the same in a
        // model
        for (int i = 0; i < includes.getLength(); i++) {
            String includeFileName = modelDir + "includes" + File.separator
                    + includes.item(i).getAttributes().getNamedItem("name").getNodeValue() + ".xml";
            System.out.println("Including " + includeFileName);
            Document includeDoc;
            try {
                includeDoc = factory.newDocumentBuilder().parse(new File(includeFileName));
            } catch (SAXException e) {
                throw new ModelGenerationException("I failed to create a DOM of this include "
                        + includeFileName, e);
            } catch (IOException e) {
                throw new ModelGenerationException("I failed to find this include file: "
                        + includeFileName, e);
            } catch (ParserConfigurationException e) {
                throw new ModelGenerationException("I failed to create a DOM of this include "
                        + includeFileName, e);
            }

            for (int j = 0; j < includeDoc.getDocumentElement().getChildNodes().getLength(); j++) {
                Node insert = includeDoc.getFirstChild().getChildNodes().item(j);
                // Insert the attributes in the include one by on to the
                // attributes tag of the model
                if (insert.getNodeType() == Node.ELEMENT_NODE) {
                    doc.getElementsByTagName("attributes").item(0).insertBefore(
                            doc.importNode(insert, true), includes.item(i));
                    doc.normalizeDocument();
                }
            }
            doc.getElementsByTagName("attributes").item(0).removeChild(includes.item(i));
        }
        doc.normalize();
    }

    public static void attribute(Node node, String name, String value) {
        Attr className = node.getOwnerDocument().createAttribute(name);
        className.setNodeValue(value);
        node.getAttributes().setNamedItem(className);
    }

    /**
     * A lot of logic is in the combination of elements and attributes in the
     * model. There are default values that are different for different
     * attributes, and more stuff that makes he modeling language easy for the
     * user, but difficult for the template builder. <br>
     * This function normalizes this. It will fill in optional (default)
     * attributes and add virtual attributes:<br>
     * - lazy;<br>
     * - className;<br>
     * - fieldName;<br>
     * - className and fieldName are derived from class and/or name. Or from the element type itself.
     * - plural;<br>
     * - scope;<br>
     * - multiplicity;<br>
     * - list;<br>
     * - trashcan;<br>
     * - notnull;<br>
     * - cascadeDelete;<br>
     * <p/>
     * For the object attribute, an extra objectId attribute is added. This is
     * the actual storage field of the object in the database.
     *
     * @param doc
     */
    public static void normalize(Document doc) throws ModelGenerationException {
        String thisObjectName
            = doc.getElementsByTagName("name").item(0).getChildNodes().item(0)
            .getNodeValue();

        if (doc.getElementsByTagName("trashcan").getLength() < 1) {
            addChildElement(doc, "trashcan", "false");
        }
        if (doc.getElementsByTagName("businessRules").getLength() > 0) {
            XPath xpath = XPathFactory.newInstance().newXPath();
            String query = "/object/businessRules/businessRule/message";
            try {
                NodeList messages = (NodeList) xpath.evaluate(query, doc, XPathConstants.NODESET);
                for (int i = 0; i < messages.getLength(); i++) {
                    Node message = messages.item(i);
                    if (message.getNodeName().equals("message")) {
                        if (message.getAttributes().getNamedItem("language") != null
                                && message.getAttributes().getNamedItem("language").getNodeValue()
                                .equals("en")) {
                            String defMesg = message.getTextContent();
                            List<String> params = new ArrayList<String>();
                            BusinessRuleMessageUtils.getBusinessRuleMessageParams(defMesg, params);
                            if (params.size() > 0) {
                                Node paramsNode = doc.createElement("params");
                                message.getParentNode().appendChild(paramsNode);
                                for (String param : params) {
                                    Node paramNode = doc.createElement("param");
                                    paramNode.setTextContent(param);
                                    paramsNode.appendChild(paramNode);
                                }
                            }
                        }
                    }
                }
            } catch (XPathExpressionException e) {
                DocPrint.print(doc);
                throw new ModelGenerationException(
                        "Failed to parse the businessrules in this model", e);
            }
        }

        try {
            NodeList attributes = XMLUtil.attributes(doc, null);
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attribute = attributes.item(i);
                if (attribute.getNodeName().equals("boolean")) {
                    attribute(attribute, "className", "Boolean");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("date")) {
                    attribute(attribute, "className", "Date");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("datetime")) {
                    attribute(attribute, "className", "Date");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("double")) {
                    attribute(attribute, "className", "Double");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("enum")) {
                    attribute(attribute, "className", attribute.getAttributes().getNamedItem(
                            "class").getNodeValue());
                    attribute(attribute, "fieldName", StringUtils.uncapitalize(attribute
                            .getAttributes().getNamedItem("class").getNodeValue()));
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("scope") == null) {
                        attribute(attribute, "scope", "local");
                    }
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("html")) {
                    attribute(attribute, "className", "String");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    if (attribute.getAttributes().getNamedItem("lazy") == null) {
                        attribute(attribute, "lazy", "true");
                    }
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("integer")) {
                    attribute(attribute, "className", "Integer");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("long")) {
                    attribute(attribute, "className", "Long");
                    attribute(attribute, "fieldName", attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    attribute(attribute, "lazy", "false");
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("object")) {
                    String className = attribute.getAttributes().getNamedItem("class")
                            .getNodeValue();
                    String fieldName = StringUtils.uncapitalize(className);
                    if (attribute.getAttributes().getNamedItem("name") != null) {
                        fieldName = attribute.getAttributes().getNamedItem("name").getNodeValue();
                    }
                    attribute(attribute, "className", className);
                    attribute(attribute, "fieldName", fieldName);
                    if (attribute.getAttributes().getNamedItem("lazy") == null) {
                        attribute(attribute, "lazy", "true");
                    }
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                    if (doc.getElementsByTagName(fieldName + "Id").getLength() == 0) {
                        Node longNode = doc.createElement("long");
                        attribute(longNode, "name", fieldName + "Id");
                        attribute(longNode, "lazy", "false");
                        attribute(longNode, "className", "Long");
                        attribute(longNode, "fieldName", fieldName + "Id");
                        attribute(longNode
                            , "notnull"
                            , attribute.getAttributes().getNamedItem("notnull").getNodeValue()
                            );
                        attribute(longNode, "parentClass", className);

                        doc.getElementsByTagName("attributes").item(0).insertBefore(longNode,
                                attribute);
                    }
                } else if (attribute.getNodeName().equals("set")) {
                    String relatedObjectName
                        = attribute.getAttributes().getNamedItem("class").getNodeValue();
                    String multiplicity = "1-to-n";
                    if (attribute.getAttributes().getNamedItem("multiplicity") != null) {
                        multiplicity = attribute.getAttributes().getNamedItem("multiplicity")
                                .getNodeValue();
                    } else {
                        attribute(attribute, "multiplicity", multiplicity);
                    }
                    if ("1-to-n".equals(multiplicity)) {
                        attribute(attribute
                            , "className"
                            , "PersistableSet<" + relatedObjectName + ">"
                            );
                    } else {
                        if (thisObjectName.compareTo(relatedObjectName) < 0) {
                            attribute(attribute
                                , "relationTableName"
                                , thisObjectName + "_" + relatedObjectName
                                );
                        } else {
                            attribute(attribute
                                , "relationTableName"
                                , relatedObjectName + "_" + thisObjectName
                                );
                        }

                        attribute(attribute
                            , "className"
                            , "PersistableNtoMSet<" + relatedObjectName + ">"
                            );
                    }

                    if (attribute.getAttributes().getNamedItem("myKey") == null) {
                        attribute(attribute
                            , "myKey"
                            , StringUtils.uncapitalize(thisObjectName) + "Id"
                            );
                    }

                    if (attribute.getAttributes().getNamedItem("theirKey") == null) {
                        attribute(attribute
                            , "theirKey"
                            , StringUtils.uncapitalize(relatedObjectName) + "Id"
                            );
                    }

                    if (attribute.getAttributes().getNamedItem("cascade") == null) {
                        attribute(attribute, "cascade", "true");
                    }

                    if (attribute.getAttributes().getNamedItem("plural") == null) {
                        attribute(attribute
                            , "plural"
                            , StringUtils.uncapitalize(relatedObjectName) + 's'
                            );
                    }
                    attribute(attribute
                        , "fieldName"
                        , attribute.getAttributes().getNamedItem("plural").getNodeValue()
                        );
                    attribute(attribute, "lazy", "true");
                } else if (attribute.getNodeName().equals("string")) {
                    attribute(attribute, "className", "String");
                    attribute(attribute
                        , "fieldName"
                        , attribute.getAttributes().getNamedItem("name").getNodeValue()
                        );
                    if (attribute.getAttributes().getNamedItem("lazy") == null) {
                        attribute(attribute, "lazy", "false");
                    }
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                } else if (attribute.getNodeName().equals("xml")) {
                    String className = StringUtils.capitalize(attribute.getAttributes()
                            .getNamedItem("name").getNodeValue());
                    if (attribute.getAttributes().getNamedItem("class") != null) {
                        className = attribute.getAttributes().getNamedItem("class").getNodeValue();
                    }
                    attribute(attribute, "className", className);
                    attribute(attribute
                        , "fieldName"
                        , StringUtils.uncapitalize(attribute.getAttributes().getNamedItem("name").getNodeValue())
                        );
                    if (attribute.getAttributes().getNamedItem("lazy") == null) {
                        attribute(attribute, "lazy", "true");
                    }
                    if (attribute.getAttributes().getNamedItem("notnull") == null) {
                        attribute(attribute, "notnull", "false");
                    }
                }
            }
        } catch (XPathExpressionException e) {
            DocPrint.print(doc);
            throw new ModelGenerationException("Failed to parse the attributes in this model", e);
        }

        doc.normalize();
    }

    /**
     * Add listScope, tableScope, formScope and lazyScope nodes to the model.<br>
     * <b>listScope</b><br>
     * In this node all the attributes that have set list='true' are added. If
     * there is no attribute with list='true' it will add the notnull='true'
     * attributes. Larger attributes: the xml and html attributes are ignored.
     * They can't be displayed in a dropdown list. <br>
     * <b>tableScope</b><br>
     * All attributes are added to the tableScope unless they are:<br>
     * - id, creationDate or updateDate;<br>
     * - set, html, xml (and not lazy='false');<br>
     * - or they have lazy='false'. <br>
     * <b>lazyScope</b><br>
     * This scope will have the attributes (excluding set) who are excluded from
     * the tableScope. <br>
     * <b>formScope</b><br>
     * This is lazyScope + tableScope.
     *
     * @param doc
     */
    public static void addScope(Document doc) throws ModelGenerationException {
        addChildElement(doc, "listScope", null);
        addChildElement(doc, "tableScope", null);
        addChildElement(doc, "lazyScope", null);
        addChildElement(doc, "formScope", null);
        try {
            NodeList attributes = XMLUtil.attributes(doc, "@list='true'");
            boolean empty = true;
            for (int i = 0; i < attributes.getLength(); i++) {
                Node insert = attributes.item(i);
                if (insert.getNodeName().equals("html") || insert.getNodeName().equals("xml")) {
                    System.out
                            .println("html and xml attribute types are not available for the Scope 'list'");
                } else if (insert.getNodeName().equals("string")
                        && Integer.parseInt(insert.getAttributes().getNamedItem("maxlength")
                        .getNodeValue()) > 255) {
                    System.out
                            .println("Larger string attibutes (maxlength > 255) are not available for the Scope 'list");
                } else {
                    doc.getElementsByTagName("listScope").item(0).appendChild(
                            doc.importNode(insert, true));
                    empty = false;
                }
            }
            if (empty) {
                attributes = XMLUtil.attributes(doc, "@notnull='true'");
                for (int i = 0; i < attributes.getLength(); i++) {
                    Node insert = attributes.item(i);
                    if (insert.getNodeName().equals("html") || insert.getNodeName().equals("xml")) {
                        System.out
                                .println("html and xml attribute types are not available for the Scope 'list'");
                    } else if (insert.getNodeName().equals("string")
                            && Integer.parseInt(insert.getAttributes().getNamedItem("maxlength")
                            .getNodeValue()) > 100) {
                        System.out
                                .println("Larger string attibutes (maxlength > 100) are not available for the Scope 'list");
                    } else {
                        doc.getElementsByTagName("listScope").item(0).appendChild(
                                doc.importNode(insert, true));
                    }
                }
            }

            attributes = XMLUtil.attributes(doc, null);
            for (int i = 0; i < attributes.getLength(); i++) {
                Node insert = attributes.item(i);
                if (insert.getNodeName().equals("set")) {
                    // Set is never (for now).
                } else if (insert.getAttributes().getNamedItem("lazy") != null
                        && "true"
                        .equals(insert.getAttributes().getNamedItem("lazy").getNodeValue())) {
                    doc.getElementsByTagName("lazyScope").item(0).appendChild(
                            doc.importNode(insert, true));
                    doc.getElementsByTagName("formScope").item(0).appendChild(
                            doc.importNode(insert, true));
                } else if (insert.getAttributes().getNamedItem("lazy") != null
                        && "false".equals(insert.getAttributes().getNamedItem("lazy")
                        .getNodeValue())) {
                    doc.getElementsByTagName("tableScope").item(0).appendChild(
                            doc.importNode(insert, true));
                    doc.getElementsByTagName("formScope").item(0).appendChild(
                            doc.importNode(insert, true));
                }
            }
            if (!doc.getElementsByTagName("lazyScope").item(0).hasChildNodes()) {
                System.out.println("There is no lazy field, remove the lazyScope node");
                doc.getFirstChild().removeChild(doc.getElementsByTagName("lazyScope").item(0));
            }
            if (!doc.getElementsByTagName("listScope").item(0).hasChildNodes()) {
                System.out
                        .println("There is no list field for dropdown list, remove the listScope node");
                doc.getFirstChild().removeChild(doc.getElementsByTagName("listScope").item(0));
            }
        } catch (XPathExpressionException e) {
            DocPrint.print(doc);
            throw new ModelGenerationException("Failed to parse the attributes in this model", e);
        }

        doc.normalize();
    }

    private static void upgradeFiles(String resourcePath, Document all)
            throws ModelGenerationException {
        File dir = new File(resourcePath + File.separator + UPGRADE);

        if (dir.exists()) {
            File[] upgrades = dir.listFiles(new FileFilter() {
                public boolean accept(File pathname) {
                    return pathname.getName().endsWith(".sql");
                }
            });
            if (upgrades.length > 0) {
                SortedSet<String> files = new TreeSet<String>();
                for (File upgrade : upgrades) {
                    files.add(upgrade.getName());
                }
                Document upgradeDoc = newDocument(null, null, "upgrades");

                for (String file : files) {
                    addChildElement(upgradeDoc, "upgrade", file);
                }
                all.getFirstChild().appendChild(all.importNode(upgradeDoc.getFirstChild(), true));
            } else {
                System.out.println("I didn't find any upgrade scripts in the upgrade directory.");
            }
        } else {
            System.out.println("Can't find " + resourcePath + File.separator + UPGRADE
                    + ", so i won't add any upgrade scripts.");
        }
    }
}
